xtask\tasks\fmt/
mod.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4mod house_rules;
5mod rustfmt;
6mod unused_deps;
7mod verify_flowey;
8mod workspace;
9
10use crate::Xtask;
11use anyhow::Context;
12use clap::Parser;
13
14/// Xtask to run various repo-specific formatting checks
15#[derive(Parser)]
16#[clap(
17    about = "Run various formatting checks",
18    disable_help_subcommand = true,
19    subcommand_value_name = "PASS",
20    subcommand_help_heading = "PASSES",
21    after_help = r#"NOTES:
22
23    For documentation on how each pass works, see the corresponding pass's help page.
24"#
25)]
26pub struct Fmt {
27    /// Attempt to fix any formatting issues
28    ///
29    /// NOTE: setting this flag disables pass-level parallelism
30    #[clap(long)]
31    fix: bool,
32
33    /// Don't run passes in parallel (avoiding potentially interweaved output)
34    #[clap(long)]
35    no_parallel: bool,
36
37    /// Only run checks on files that are currently diffed
38    #[clap(long)]
39    only_diffed: bool,
40
41    /// Run multiple formatting passes at once
42    #[clap(long)]
43    pass: Vec<PassName>,
44
45    /// Run a single specific formatting pass
46    #[clap(subcommand)]
47    passes: Option<Passes>,
48}
49
50#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)]
51enum PassName {
52    HouseRules,
53    Rustfmt,
54    UnusedDeps,
55    VerifyWorkspace,
56    VerifyFuzzers,
57    VerifyFlowey,
58}
59
60impl PassName {
61    fn kebab_case(self) -> &'static str {
62        match self {
63            PassName::HouseRules => "house-rules",
64            PassName::Rustfmt => "rustfmt",
65            PassName::UnusedDeps => "unused-deps",
66            PassName::VerifyWorkspace => "verify-workspace",
67            PassName::VerifyFuzzers => "verify-fuzzers",
68            PassName::VerifyFlowey => "verify-flowey",
69        }
70    }
71}
72
73#[derive(clap::Subcommand)]
74enum Passes {
75    HouseRules(house_rules::HouseRules),
76    Rustfmt(rustfmt::Rustfmt),
77    UnusedDeps(unused_deps::UnusedDeps),
78    VerifyWorkspace(workspace::VerifyWorkspace),
79    VerifyFuzzers(crate::tasks::fuzz::VerifyFuzzers),
80    VerifyFlowey(verify_flowey::VerifyFlowey),
81}
82
83impl Xtask for Fmt {
84    fn run(self, ctx: crate::XtaskCtx) -> anyhow::Result<()> {
85        // short-circuit if a specific pass was requested
86        if let Some(pass) = self.passes {
87            if !self.pass.is_empty() {
88                anyhow::bail!("cannot use `--pass` when invoking pass directly")
89            }
90
91            match pass {
92                Passes::UnusedDeps(cmd) => cmd.run(ctx)?,
93                Passes::Rustfmt(cmd) => cmd.run(ctx)?,
94                Passes::HouseRules(cmd) => cmd.run(ctx)?,
95                Passes::VerifyWorkspace(cmd) => cmd.run(ctx)?,
96                Passes::VerifyFuzzers(cmd) => cmd.run(ctx)?,
97                Passes::VerifyFlowey(cmd) => cmd.run(ctx)?,
98            }
99
100            return Ok(());
101        }
102
103        // otherwise, run all the formatting passes
104        let tasks: Vec<Box<dyn FnOnce() -> anyhow::Result<()> + Send>> = {
105            fn wrapper(
106                ctx: &crate::XtaskCtx,
107                name: &str,
108                func: impl FnOnce(crate::XtaskCtx) -> anyhow::Result<()> + Send + 'static,
109            ) -> Box<dyn FnOnce() -> anyhow::Result<()> + Send> {
110                let ctx = ctx.clone();
111                let name = name.to_string();
112
113                Box::new(move || {
114                    let start_time = std::time::Instant::now();
115                    log::info!("[checking] {}", name);
116                    let res = func(ctx).context(format!("while running {name}"));
117                    log::info!(
118                        "[complete] {} ({:.2?})",
119                        name,
120                        std::time::Instant::now() - start_time
121                    );
122                    res
123                })
124            }
125
126            let fix = self.fix;
127            let only_diffed = self.only_diffed;
128
129            let passes = if !self.pass.is_empty() {
130                let mut passes = self.pass.clone();
131                passes.sort();
132                passes.dedup_by(|a, b| a == b);
133                passes
134            } else {
135                // run all of them by default
136                vec![
137                    PassName::HouseRules,
138                    PassName::Rustfmt,
139                    PassName::UnusedDeps,
140                    PassName::VerifyWorkspace,
141                    PassName::VerifyFuzzers,
142                    PassName::VerifyFlowey,
143                ]
144            };
145
146            passes
147                .into_iter()
148                .map(|pass| {
149                    let name = pass.kebab_case();
150                    match pass {
151                        PassName::HouseRules => wrapper(&ctx, name, {
152                            move |ctx| {
153                                house_rules::HouseRules::all_passes(fix, only_diffed).run(ctx)
154                            }
155                        }),
156                        PassName::Rustfmt => wrapper(&ctx, name, {
157                            move |ctx| rustfmt::Rustfmt::new(fix, only_diffed).run(ctx)
158                        }),
159                        PassName::UnusedDeps => wrapper(&ctx, name, {
160                            move |ctx| unused_deps::UnusedDeps { fix }.run(ctx)
161                        }),
162                        PassName::VerifyWorkspace => wrapper(&ctx, name, {
163                            move |ctx| workspace::VerifyWorkspace.run(ctx)
164                        }),
165                        PassName::VerifyFuzzers => wrapper(&ctx, name, {
166                            move |ctx| crate::tasks::fuzz::VerifyFuzzers.run(ctx)
167                        }),
168                        PassName::VerifyFlowey => wrapper(&ctx, name, {
169                            move |ctx| verify_flowey::VerifyFlowey::new(fix).run(ctx)
170                        }),
171                    }
172                })
173                .collect()
174        };
175
176        let results: Vec<_> = if self.fix || self.no_parallel {
177            tasks.into_iter().map(|f| (f)()).collect()
178        } else {
179            tasks
180                .into_iter()
181                .map(std::thread::spawn)
182                .collect::<Vec<_>>()
183                .into_iter()
184                .map(|j| j.join().unwrap())
185                .collect()
186        };
187
188        for res in results.iter() {
189            if let Err(e) = res {
190                log::error!("{:#}", e);
191            }
192        }
193
194        if results.iter().any(|res| res.is_err()) && !self.fix {
195            log::error!(
196                "run `cargo xtask fmt{}{} --fix`",
197                if self.only_diffed {
198                    " --only-diffed"
199                } else {
200                    ""
201                },
202                if !self.pass.is_empty() {
203                    self.pass
204                        .into_iter()
205                        .map(|pass| format!(" --pass {}", pass.kebab_case()))
206                        .collect::<Vec<_>>()
207                        .join("")
208                } else {
209                    "".into()
210                }
211            );
212            Err(anyhow::anyhow!("found formatting errors"))
213        } else {
214            Ok(())
215        }
216    }
217}